/*   The MIT License
 *   
 *   Carina Engine
 *   Copyright (c) 2009 2010 2011 2012 Zdravko Velinov
 *   
 *   Permission is hereby granted, free of charge, to any person obtaining a copy
 *   of this software and associated documentation files (the "Software"), to deal
 *   in the Software without restriction, including without limitation the rights
 *   to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 *   copies of the Software, and to permit persons to whom the Software is
 *   furnished to do so, subject to the following conditions:
 *
 *   The above copyright notice and this permission notice shall be included in
 *   all copies or substantial portions of the Software.
 *
 *   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 *   IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 *   FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 *   AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 *   LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 *   OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 *   THE SOFTWARE.
 */

#include "carina/gl-renderer.hh"
#include "carina/common/logging.hh"
#include "carina/utils.hh"
#include "carina/gl-effect-generator.hh"

#include <tuple>
#include <unordered_map>
#include <cstring>

#ifdef _MSC_VER
#   pragma warning(disable : 4503)
#endif

namespace Carina
{
GLSLShaderCompiler::GLSLShaderCompiler(GLRenderer& renderer)
    :   ShaderCompiler("GLSL"),
        m_Renderer(renderer)
{
}

GLSLShaderCompiler::~GLSLShaderCompiler()
{
}

ShaderProgramPtr GLSLShaderCompiler::compileShaderProgram(const string& filename) const
{
    return make_aligned_shared<GLShaderProgram>(m_Renderer, filename);
}

GLVideoBuffer::GLVideoBuffer(GLenum type, GLsizeiptr size, const GLvoid* data, GLenum usage)
    :   m_Type(type)
{
    glGenBuffers(1, &m_BufferID);
    glBindBuffer(type, m_BufferID);
    glBufferData(type, size, data, usage);
}

GLVideoBuffer::~GLVideoBuffer()
{
    glDeleteBuffers(1, &m_BufferID);
}

void GLVideoBuffer::bind() const
{
    glBindBuffer(m_Type, m_BufferID);
}

GLInputLayout::GLInputLayout(const GLInputElementArray& ielems)
    :   m_InputElems(ielems)
{
}

void GLInputLayout::setup(const ShaderProgramPtr& prog, GLsizei stride, GLsizei offset) const
{
    for(size_t i = 0; i < m_InputElems.size(); ++i)
    {
        glEnableVertexAttribArray(m_InputElems[i].location); 
        glVertexAttribPointer(m_InputElems[i].location,
                              m_InputElems[i].size,
                              m_InputElems[i].type,
                              m_InputElems[i].normalized,
                              stride, reinterpret_cast<GLvoid*>(offset+m_InputElems[i].offset));
    }
}

GLTexture::GLTexture(const TextureDescription& desc, const void* data, int flags)
{
    struct TexInfo
    {
        GLint   internalFormat;
        GLenum  format,
                type;
        TexInfo() {}
        TexInfo(GLint intformat, GLenum _format, GLenum _type)
            :   internalFormat(intformat),
                format(_format),
                type(_type) {}
    } tex_info;

    switch(desc.Format)
    {
//  case VT_UNKNOWN: break;
    case VT_R32F: tex_info = TexInfo(GL_R32F, GL_RED, GL_FLOAT); break;
    case VT_RG32F: tex_info = TexInfo(GL_RG32F, GL_RG, GL_FLOAT); break;
    case VT_RGB32F: tex_info = TexInfo(GL_RGB32F, GL_RGB, GL_FLOAT); break;
    case VT_RGBA32F: tex_info = TexInfo(GL_RGBA32F, GL_RGBA, GL_FLOAT); break;
    case VT_R16F: tex_info = TexInfo(GL_R16F, GL_RED, GL_FLOAT); break;
    case VT_RG16F: tex_info = TexInfo(GL_RG16F, GL_RG, GL_FLOAT); break;
//  case VT_RGB16F: break; 
    case VT_RGBA16F: tex_info = TexInfo(GL_RGBA16F, GL_RGBA, GL_FLOAT); break;
//  case VT_R32: tex_info = TexInfo(GL_RED, GL_RED, GL_INT); break;
//  case VT_RG32: tex_info = TexInfo(GL_RG, GL_RG, GL_INT); break;
//  case VT_RGB32: tex_info = TexInfo(GL_RGB, GL_RGB, GL_INT); break;
//  case VT_RGBA32: tex_info = TexInfo(GL_RGBA, GL_RGBA, GL_INT); break;
    case VT_R16_SNORM: tex_info = TexInfo(GL_R16_SNORM, GL_RED, GL_SHORT); break;
    case VT_RG16_SNORM: tex_info = TexInfo(GL_RG16_SNORM, GL_RG, GL_SHORT); break;
//  case VT_RGB16_SNORM: break;
    case VT_RGBA16_SNORM: tex_info = TexInfo(GL_RGBA16_SNORM, GL_RGBA, GL_SHORT); break;
    case VT_R8_SNORM: tex_info = TexInfo(GL_R8_SNORM, GL_RED, GL_BYTE); break;
    case VT_RG8_SNORM: tex_info = TexInfo(GL_RG8_SNORM, GL_RG, GL_BYTE); break;
//  case VT_RGB8_SNORM: break;
    case VT_RGBA8_SNORM: tex_info = TexInfo(GL_RGBA8_SNORM, GL_RGBA, GL_BYTE); break;
//  case VT_R32: tex_info = TexInfo(GL_RED, GL_RED, GL_UNSIGNED_INT); break;
//  case VT_RG32: tex_info = TexInfo(GL_RG, GL_RG, GL_UNSIGNED_INT); break;
//  case VT_RGB32: tex_info = TexInfo(GL_RGB, GL_RGB, GL_UNSIGNED_INT); break;
//  case VT_RGBA32: tex_info = TexInfo(GL_RGBA, GL_RGBA, GL_UNSIGNED_INT); break;
    case VT_R16_UNORM: tex_info = TexInfo(GL_R16, GL_RED, GL_UNSIGNED_SHORT); break;
    case VT_RG16_UNORM: tex_info = TexInfo(GL_RG16, GL_RG, GL_UNSIGNED_SHORT); break;
//  case VT_RGB16_UNORM: tex_info = TexInfo(GL_RGB, GL_RGB, GL_UNSIGNED_SHORT); break;
    case VT_RGBA16_UNORM: tex_info = TexInfo(GL_RGBA16, GL_RGBA, GL_UNSIGNED_SHORT); break;
    case VT_R8_UNORM: tex_info = TexInfo(GL_R8, GL_RED, GL_UNSIGNED_BYTE); break;
    case VT_RG8_UNORM: tex_info = TexInfo(GL_RG8, GL_RG, GL_UNSIGNED_BYTE); break;
//  case VT_RGB8_UNORM: break;
    case VT_RGBA8_UNORM: tex_info = TexInfo(GL_RGBA8, GL_RGBA, GL_UNSIGNED_BYTE); break;
    case VT_D16: tex_info = TexInfo(GL_DEPTH_COMPONENT16, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT); break;
    case VT_D24S8: tex_info = TexInfo(GL_DEPTH24_STENCIL8, GL_DEPTH_STENCIL, GL_UNSIGNED_INT_24_8); break;
    case VT_D32: tex_info = TexInfo(GL_DEPTH_COMPONENT32, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT); break;
    case VT_R10G10B10A2: tex_info = TexInfo(GL_RGBA, GL_RGBA, GL_INT_2_10_10_10_REV); break;
    case VT_UINT_R10G10B10A2: tex_info = TexInfo(GL_RGBA, GL_RGBA, GL_UNSIGNED_INT_10_10_10_2); break;
    default:
        CE_ASSERT(false, "Unsupported texture format."); break;
    }

    CHECK_OPENGL();
    glGenTextures(1, &m_TexId);
    CE_ASSERT(desc.Width && desc.Height && desc.Depth, "Texture should have all dimensions different than zero");
    CE_ASSERT((flags & CE_TEXTURE_ARRAY) == 0 || (flags & CE_TEXTURE_CUBE) == 0, "TODO: Unsupported");
    if(flags & CE_TEXTURE_CUBE)
    {
        CE_ASSERT(desc.Depth == 6, "Unexpected number of faces for cube map. Should be 6.");
        CE_ASSERT(false, "TODO: Unimplemented"); // TODO: cube mapping
        //m_Target = GL_TEXTURE_CUBE_MAP;
        //glBindTexture(m_Target, m_TexId);
        //glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, tex_info.internalFormat, width, height, 0, tex_info.format, tex_info.type, data);
        //glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, tex_info.internalFormat, width, height, 0, tex_info.format, tex_info.type, data + width*height*tex_info.size);
        //glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, tex_info.internalFormat, width, height, 0, tex_info.format, tex_info.type, data + 2*width*height*tex_info.size);
        //glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, tex_info.internalFormat, width, height, 0, tex_info.format, tex_info.type, data + 3*width*height*tex_info.size);
        //glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, tex_info.internalFormat, width, height, 0, tex_info.format, tex_info.type, data + 4*width*height*tex_info.size);
        //glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, tex_info.internalFormat, width, height, 0, tex_info.format, tex_info.type, data + 5*width*height*tex_info.size);
    }
    else if(desc.Depth > 1)
    {
        if(flags & CE_TEXTURE_ARRAY)
            m_Target = GL_TEXTURE_2D_ARRAY;
        else
            m_Target = GL_TEXTURE_3D;
        
        glBindTexture(m_Target, m_TexId);
        if(desc.Samples > 1)
            glTexImage3DMultisample(m_Target, desc.Samples, tex_info.internalFormat, desc.Width, desc.Height, desc.Depth, GL_TRUE);
        else
            glTexImage3D(m_Target, 0, tex_info.internalFormat, desc.Width, desc.Height, desc.Depth, 0, tex_info.format, tex_info.type, data);
    }
    else if(desc.Height)
    {
        if(flags & CE_TEXTURE_ARRAY)
            m_Target = GL_TEXTURE_1D_ARRAY;
        else
            m_Target = GL_TEXTURE_2D;

        glBindTexture(m_Target, m_TexId);
        if(desc.Samples > 1)
            glTexImage2DMultisample(m_Target, desc.Samples, tex_info.internalFormat, desc.Width, desc.Height, GL_TRUE), assert(!data && "Uploading data on multisampled textures is unsupported");
        else
            glTexImage2D(m_Target, 0, tex_info.internalFormat, desc.Width, desc.Height, 0, tex_info.format, tex_info.type, data);
    }
    else
    {
        m_Target = GL_TEXTURE_1D;
        glBindTexture(m_Target, m_TexId);
        CE_ASSERT(desc.Samples <= 1, "Multisampling is unsupported for 1D textures");
        glTexImage1D(m_Target, 0, tex_info.internalFormat, desc.Width, 0, tex_info.format, tex_info.type, data);
    }

    if(data && (flags & CE_GENERATE_MIPS))
        glGenerateMipmap(GL_TEXTURE_2D);
    CHECK_OPENGL();
}

GLTexture::~GLTexture()
{
    glDeleteTextures(1, &m_TexId);
}

void GLTexture::bind() const
{
    glBindTexture(GL_TEXTURE_2D, m_TexId);
}

void GLTexture::bind(GLenum n) const
{
    glActiveTexture(n);
    glBindTexture(GL_TEXTURE_2D, m_TexId);
}

GLint GLTexture::getWidth(GLint level) const
{
    GLint w;
    glGetTexLevelParameteriv(m_Target, level, GL_TEXTURE_WIDTH, &w);
    return w;
}

GLint GLTexture::getHeight(GLint level) const
{
    GLint h;
    glGetTexLevelParameteriv(m_Target, level, GL_TEXTURE_HEIGHT, &h);
    return h;
}

#define CE_DEBUG_GLSL_APPEND_SOURCE

GLShader::GLShader(GLenum type, const string& source)
    :   m_Id(glCreateShader(static_cast<GLenum>(type)))
{
    GLint status;
    const char* cstr = source.c_str();
    glShaderSource(m_Id, 1, &cstr, nullptr);
    glCompileShader(m_Id);
    glGetShaderiv(m_Id, GL_COMPILE_STATUS, &status);
    if(status == GL_FALSE) {
        GLint len;
        DynamicArray<GLchar> txt;

        glGetShaderiv(m_Id, GL_INFO_LOG_LENGTH, &len);
        txt.resize(len);
        glGetShaderInfoLog(m_Id, len, nullptr, txt.get());
        string error(txt.get(), len-1);
        
    #ifdef CE_DEBUG_GLSL_APPEND_SOURCE
        error += "\n\nSource code\n"
                 "=======================================\n\n";
        error += source;
    #endif
        string shader_type;
        switch(type)
        {
        case GL_VERTEX_SHADER: shader_type = "Vertex shader:\n"; break;
        case GL_TESS_CONTROL_SHADER: shader_type = "Tessellation control shader:\n"; break;
        case GL_TESS_EVALUATION_SHADER: shader_type = "Tessellation evaluation shader:\n"; break;
        case GL_GEOMETRY_SHADER: shader_type = "Geometry shader:\n"; break;
        case GL_FRAGMENT_SHADER: shader_type = "Fragment shader:\n"; break;
        default: CE_ASSERT(false, "Unsupported shader type."); break;
        }
        THROW_EXCEPTION(shader_type + error);
    }
}

GLShader::~GLShader()
{
    glDeleteShader(m_Id);
}

GLShaderProgram::GLShaderProgram(GLRenderer& rend, const string& filename)
#ifdef CE_RELOADABLE_RESOURCES
    :   m_Renderer(rend)
#endif
{
    compile(rend, filename);
}

bool IsInvalidSamplerParamPair(GLenum stname, GLenum stval)
{
    switch(stname)
    {
    case GL_TEXTURE_MIN_FILTER:
        if(stval != GL_NEAREST &&
           stval != GL_LINEAR &&
           stval != GL_NEAREST_MIPMAP_NEAREST &&
           stval != GL_LINEAR_MIPMAP_NEAREST &&
           stval != GL_NEAREST_MIPMAP_LINEAR &&
           stval != GL_LINEAR_MIPMAP_LINEAR)
            return true;
        break;
    case GL_TEXTURE_MAG_FILTER:
        if(stval != GL_NEAREST &&
           stval != GL_LINEAR)
            return true;
        break;
    case GL_TEXTURE_WRAP_S:
    case GL_TEXTURE_WRAP_T:
    case GL_TEXTURE_WRAP_R:
        if(stval != GL_CLAMP_TO_EDGE &&
           stval != GL_CLAMP_TO_BORDER &&
           stval != GL_MIRRORED_REPEAT &&
           stval != GL_REPEAT )
            return true;
        break;
    default:
        return true;
        break;
    }

    return false;
}

// TODO: The whole thing is a little bit slow because of the whole string passing
void GLShaderProgram::compile(GLRenderer& rend, const string& filename)
{
    // Hacks for ultra stupid compilers
    auto& current_technique = m_CurrentTechnique;
    auto& technique_map = m_TechniqueMap;
    auto transaction = CreateTransaction([&current_technique, &technique_map]()
                                         {
                                             current_technique = nullptr;
                                             technique_map.clear();
                                         });
    Effect::EffectDescription effect;
    if(!GLFX::LoadEffect(filename, rend.getIncludeLoader(), effect))
        return;

    typedef std::unordered_map<string, GLShaderPtr> CompiledShaders;
    CompiledShaders shaders;

    typedef std::unordered_map<string, GLSamplerPtr> Samplers;
    Samplers samplers;

    for(size_t i = 0, iend = effect.getSamplerCount(); i < iend; ++i)
    {
        auto& sampler = effect.getSampler(i);
        auto& sampler_inst = samplers[sampler.getName()] = make_aligned_shared<GLSampler>();
        for(size_t j = 0, jend = sampler.getParameterCount(); j < jend; ++j)
        {
            auto& param = sampler.getParameter(j);
            auto state_name = param.getName();
            auto state_value = param.getValue();
            GLenum stname = rend.getEnum(state_name),
                   stval = rend.getEnum(state_value);
            if(IsInvalidSamplerParamPair(stname, stval))
            {
                Log::stream(CE_LOG_ERROR) << "invalid state pair(" << state_name << ", " << state_value << ") in file: " << filename << std::endl;
                return;
            }
            sampler_inst->setParameter(stname, stval);
        }
    }

    for(size_t i = 0, iend = effect.getTechniqueCount(); i < iend; ++i)
    {
        auto& technique = effect.getTechnique(i);
        try
        {
            Samplers samp_list;
            
            auto& technique_inst = m_TechniqueMap[technique.getName()] = make_aligned_shared<GLTechnique>();

            CE_ASSERT(technique.getPassCount(), "Expected at least a single valid pass");
            auto& pass = technique.getPass(0); // TODO: more than a single pass
            for(size_t k = 0, kend = pass.getAttachedShaderCount(); k < kend; ++k)
            {
                auto& shader_desc = pass.getAttachedShaderName(k);
                string shader_name = shader_desc.getName();
                auto iter = shaders.find(shader_name);
                GLShaderPtr shader_inst;
                if(iter == shaders.end())
                {
                    size_t w, wend;
                    for(w = 0, wend = effect.getShaderCount(); w < wend; ++w)
                    {
                        auto& shader = effect.getShader(w);
                        if(shader.getName() == shader_name)
                            break;
                    }
                    CE_ASSERT(w == wend, "Expected reference to shader. Else the parser is buggy.");
                    auto& shader = effect.getShader(w);
                    GLenum shader_type = 0;
                    switch(shader.getShaderType())
                    {
                    case Effect::CE_EFFECT_VERTEX_SHADER: shader_type = GL_VERTEX_SHADER; break;
                    case Effect::CE_EFFECT_TESSELLATION_CONTROL_SHADER: shader_type = GL_TESS_CONTROL_SHADER; break;
                    case Effect::CE_EFFECT_TESSELLATION_EVALUATION_SHADER: shader_type = GL_TESS_EVALUATION_SHADER; break;
                    case Effect::CE_EFFECT_GEOMETRY_SHADER: shader_type = GL_GEOMETRY_SHADER; break;
                    case Effect::CE_EFFECT_FRAGMENT_SHADER: shader_type = GL_FRAGMENT_SHADER; break;
                    default: CE_ASSERT(false, "Unknown shader type.");
                    }
                    shaders[shader.getName()] = make_aligned_shared<GLShader>(shader_type, shader_desc.getAdditionalOptions() + shader.getContents());
                    for(size_t m = 0, mend = shader.getSamplerCount(); m < mend; ++m)
                    {
                        auto sampler_name = shader.getSamplerName(m);
                        auto sampler_iter = samplers.find(sampler_name);
                        CE_ASSERT(sampler_iter != samplers.end(), "Expected valid sampler. Definitely buggy code.");
                        samp_list[sampler_name] = sampler_iter->second;
                    }
                }
                else
                    shader_inst = iter->second;
                technique_inst->attach(*shader_inst);
            }

            size_t j, jend, k, kend;
            for(j = 0, jend = pass.getAttachedShaderCount(); j < jend; ++jend)
            {
                auto shader_name = pass.getAttachedShaderName(j).getName();
                for(k = 0, kend = effect.getShaderCount(); k < kend; ++k)
                    if(shader_name == effect.getShader(k).getName())
                        break;
                CE_ASSERT(k != kend, "Expecting valid shader.");
                auto& shader = effect.getShader(k);
                if(shader.getShaderType() == Effect::CE_EFFECT_VERTEX_SHADER)
                    break;
            }
            if(j == jend)
            {
                Log::stream(CE_LOG_ERROR) << "Expecting valid vertex shader." << std::endl;
                return;
            }
            auto& shader = effect.getShader(k);

            for(j = 0, jend = shader.getInputParameterCount(); j < jend; ++j)
            {
                auto& param = shader.getInputParameter(j);
                technique_inst->mapAttrib(j, param.getName(), param.getSemantic());
            }

            technique_inst->link();

            CHECK_OPENGL();
            size_t texunit = 0, sampsize;
            for(auto k = samp_list.begin(), kend = samp_list.end(); k != kend; ++k, texunit += sampsize)
            {
                sampsize = technique_inst->getSamplerSize(k->first);
                technique_inst->addTextureVar(k->first, make_aligned_shared<GLTextureVar>(texunit, k->second));
            }

            technique_inst->update();
        }
        catch(const Carina::Exception& e)
        {
            THROW_EXTENDED_EXCEPTION(e, ": technique:" + technique.getName() + ": " + filename);
        }
    }
    

    if(m_TechniqueMap.empty())
    {
        Log::stream(CE_LOG_ERROR) << "No techniques present: " << filename << std::endl;
        return;
    }

    m_CurrentTechnique = m_TechniqueMap.begin()->second;
    transaction.commit();
}

GLShaderProgram::~GLShaderProgram()
{
}

#ifdef CE_RELOADABLE_RESOURCES
void GLShaderProgram::reload(const string& filename)
{
    m_TechniqueMap.clear();
    compile(m_Renderer, filename);
}
#endif

void GLShaderProgram::use() const
{
    // Don't render when there is not any working shader program
    if(m_CurrentTechnique)
        m_CurrentTechnique->use();
}

GLuint GLShaderProgram::getAttribLocation(const string& name) const
{
    return m_CurrentTechnique ? m_CurrentTechnique->getAttribLocation(name) : -1;
}

void GLShaderProgram::useTechnique(const string& name)
{
    TechniqueMap::iterator i = m_TechniqueMap.find(name);
    if(i == m_TechniqueMap.end())
    {
        Log::message(CE_LOG_ERROR, "Unknown technique");
        return;
    }
    m_CurrentTechnique = i->second;
}

UniformVarPtr GLShaderProgram::getVariable(const string& name)
{
    if(m_CurrentTechnique)
        return m_CurrentTechnique->getVariable(name);
    return UniformVarPtr();
}
TextureVarPtr GLShaderProgram::getTexture(const string& name)
{
    if(m_CurrentTechnique)
        return m_CurrentTechnique->getTexture(name);
    return TextureVarPtr();
}

void GLShaderProgram::getVariableType(const string& name, UniformVarTypeDesc& desc)
{
    if(m_CurrentTechnique)
        m_CurrentTechnique->getVariableType(name, desc);
    desc = { 0, CE_UVT_UNKNOWN };
}

GLTechnique::GLTechnique()
    :   m_Id(glCreateProgram()) {}

GLTechnique::~GLTechnique()
{
    glDeleteProgram(m_Id);
}

void GLTechnique::getVariableType(const string& name, UniformVarTypeDesc& desc)
{
    GLint num_uniforms;
    glGetProgramiv(m_Id, GL_ACTIVE_UNIFORMS, &num_uniforms);
    GLint uniform_max_len;
    glGetProgramiv(m_Id, GL_ACTIVE_UNIFORM_MAX_LENGTH, &uniform_max_len);
    DynamicArray<char> uniform_name(uniform_max_len);

    GLint   size;
    GLenum  type;
    for(GLint i = 0; i < num_uniforms; ++i)
    {
        glGetActiveUniform(m_Id, i, uniform_max_len, nullptr, &size, &type, uniform_name.get());
        auto len = strlen(uniform_name.get());
        if(len > sizeof("[0]") && !strcmp(uniform_name.get() + len - sizeof("[0]") + 1, "[0]"))
            uniform_name[len - sizeof("[0]") + 1] = 0;
        if(name == uniform_name.get())
        {
            desc.size = size > 1 ? size : 0;
            switch(type)
            {
            case GL_FLOAT: desc.type = CE_UVT_FLOAT; break;
            case GL_FLOAT_VEC2: desc.type = CE_UVT_VEC2; break;
            case GL_FLOAT_VEC3: desc.type = CE_UVT_VEC3; break;
            case GL_FLOAT_VEC4: desc.type = CE_UVT_VEC4; break;
            case GL_INT: desc.type = CE_UVT_INT; break;
            case GL_INT_VEC2: desc.type = CE_UVT_IVEC2; break;
            case GL_INT_VEC3: desc.type = CE_UVT_IVEC3; break;
            case GL_INT_VEC4: desc.type = CE_UVT_IVEC4; break;
            case GL_UNSIGNED_INT: desc.type = CE_UVT_UINT; break;
            case GL_UNSIGNED_INT_VEC2: desc.type = CE_UVT_UVEC2; break;
            case GL_UNSIGNED_INT_VEC3: desc.type = CE_UVT_UVEC3; break;
            case GL_UNSIGNED_INT_VEC4: desc.type = CE_UVT_UVEC4; break;
            case GL_BOOL: desc.type = CE_UVT_BOOL; break;
            case GL_BOOL_VEC2: desc.type = CE_UVT_BVEC2; break;
            case GL_BOOL_VEC3: desc.type = CE_UVT_BVEC3; break;
            case GL_BOOL_VEC4: desc.type = CE_UVT_BVEC4; break;
            case GL_FLOAT_MAT2: desc.type = CE_UVT_MAT2; break;
            case GL_FLOAT_MAT3: desc.type = CE_UVT_MAT3; break;
            case GL_FLOAT_MAT4: desc.type = CE_UVT_MAT4; break;
            case GL_FLOAT_MAT2x3: desc.type = CE_UVT_MAT2x3; break;
            case GL_FLOAT_MAT2x4: desc.type = CE_UVT_MAT2x4; break;
            case GL_FLOAT_MAT3x2: desc.type = CE_UVT_MAT3x2; break;
            case GL_FLOAT_MAT3x4: desc.type = CE_UVT_MAT3x4; break;
            case GL_FLOAT_MAT4x2: desc.type = CE_UVT_MAT4x2; break;
            case GL_FLOAT_MAT4x3: desc.type = CE_UVT_MAT4x3; break;
            default:
                THROW_EXCEPTION("unknown data type: " + convert_value(type) + ": " + name);
            }
            return;
        }
    }
    THROW_EXCEPTION("unknown variable: " + name);
}

size_t GLTechnique::getSamplerSize(const string& name)
{
    GLint num_uniforms;
    glGetProgramiv(m_Id, GL_ACTIVE_UNIFORMS, &num_uniforms);
    GLint uniform_max_len;
    glGetProgramiv(m_Id, GL_ACTIVE_UNIFORM_MAX_LENGTH, &uniform_max_len);
    DynamicArray<char> uniform_name(uniform_max_len);

    GLint   size;
    GLenum  type;
    for(GLint i = 0; i < num_uniforms; ++i)
    {
        glGetActiveUniform(m_Id, i, uniform_max_len, nullptr, &size, &type, uniform_name.get());
        auto len = strlen(uniform_name.get());
        if(len > sizeof("[0]") && !strcmp(uniform_name.get() + len - sizeof("[0]") + 1, "[0]"))
            uniform_name[len - sizeof("[0]") + 1] = 0;
        if(name == uniform_name.get())
        {
            switch(type)
            {
            case GL_SAMPLER_1D:
            case GL_SAMPLER_2D:
            case GL_SAMPLER_3D:
            case GL_SAMPLER_CUBE:
            case GL_SAMPLER_1D_SHADOW:
            case GL_SAMPLER_2D_SHADOW:
            case GL_SAMPLER_1D_ARRAY:
            case GL_SAMPLER_2D_ARRAY:
            case GL_SAMPLER_1D_ARRAY_SHADOW:
            case GL_SAMPLER_2D_ARRAY_SHADOW:
            case GL_SAMPLER_2D_MULTISAMPLE:
            case GL_SAMPLER_2D_MULTISAMPLE_ARRAY:
            case GL_SAMPLER_CUBE_SHADOW:
            case GL_SAMPLER_BUFFER:
            case GL_SAMPLER_2D_RECT:
            case GL_SAMPLER_2D_RECT_SHADOW:
            case GL_INT_SAMPLER_1D:
            case GL_INT_SAMPLER_2D:
            case GL_INT_SAMPLER_3D:
            case GL_INT_SAMPLER_CUBE:
            case GL_INT_SAMPLER_1D_ARRAY:
            case GL_INT_SAMPLER_2D_ARRAY:
            case GL_INT_SAMPLER_2D_MULTISAMPLE:
            case GL_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
            case GL_INT_SAMPLER_BUFFER:
            case GL_INT_SAMPLER_2D_RECT:
            case GL_UNSIGNED_INT_SAMPLER_1D:
            case GL_UNSIGNED_INT_SAMPLER_2D:
            case GL_UNSIGNED_INT_SAMPLER_3D:
            case GL_UNSIGNED_INT_SAMPLER_CUBE:
            case GL_UNSIGNED_INT_SAMPLER_1D_ARRAY:
            case GL_UNSIGNED_INT_SAMPLER_2D_ARRAY:
            case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE:
            case GL_UNSIGNED_INT_SAMPLER_2D_MULTISAMPLE_ARRAY:
            case GL_UNSIGNED_INT_SAMPLER_BUFFER:
            case GL_UNSIGNED_INT_SAMPLER_2D_RECT:
                return size;
            default:
                THROW_EXCEPTION("Unsupported sampler type: " + convert_value(type) + ": " + name);
            }
        }
    }
    THROW_EXCEPTION("Unknown variable: " + name +
                    ". It is possible that it was optimized out by the compiler. Please check whether it actually reaches the output.");
}

void GLTechnique::attach(GLShader& shader)
{
    glAttachShader(m_Id, shader.m_Id);
}

void GLTechnique::link()
{
    GLint status;
    glLinkProgram(m_Id);
    glGetProgramiv(m_Id, GL_LINK_STATUS, &status);
    if(status == GL_FALSE)
    {
        GLint len;
        glGetProgramiv(m_Id, GL_INFO_LOG_LENGTH, &len);
        DynamicArray<GLchar> msg;
        msg.resize(len);
        glGetProgramInfoLog(m_Id, len, nullptr, msg.get());
        string error(msg.get(), len);
        THROW_EXCEPTION(error);
    }

    update();
}

void GLTechnique::update()
{
    // Extracts the locations of uniform texture sampler variables.
    GLint loc;
    for(SamplerMap::iterator i = m_SamplerMap.begin();
        i != m_SamplerMap.end(); ++i)
    {
        loc = glGetUniformLocation(m_Id, i->first.c_str());
        if(loc < 0)
            THROW_EXCEPTION("unknown uniform variable: " + i->first);
        i->second->updateLocation(loc);
    }
}

void GLTechnique::use() const
{
    glUseProgram(m_Id);
}

void GLTechnique::validate()
{
    assert(glIsProgram(m_Id) == GL_TRUE);
    GLint status;
    glValidateProgram(m_Id);
    glGetProgramiv(m_Id, GL_VALIDATE_STATUS, &status);
    if(status == GL_FALSE)
    {
        GLint len;
        glGetProgramiv(m_Id, GL_INFO_LOG_LENGTH, &len);
        DynamicArray<GLchar> msg;
        msg.resize(len);
        glGetProgramInfoLog(m_Id, len, nullptr, msg.get());
        string error(msg.get(), len);
        THROW_EXCEPTION(error);
    }
}

UniformVarPtr GLTechnique::getVariable(const string& name)
{
    GLint loc = glGetUniformLocation(m_Id, name.c_str());
    if(loc < 0)
        THROW_EXCEPTION("unknown uniform variable: " + name);
    return make_aligned_shared<GLUniformVar>(m_Id, loc);
}

TextureVarPtr GLTechnique::getTexture(const string& name)
{
    SamplerMap::iterator i = m_SamplerMap.find(name);
    if(i == m_SamplerMap.end())
        THROW_EXCEPTION("unknown texture sampler: " + name);
    return i->second;
}

void GLTechnique::addTextureVar(const string& name, const GLTextureVarPtr& ptr)
{
    m_SamplerMap[name] = ptr;
}

GLuint GLTechnique::getAttribLocation(const string& name) const
{
    AttributeMap::const_iterator i = m_AttrMap.find(name);
    if(i == m_AttrMap.end())
        THROW_EXCEPTION("unknown attribute: " + name);
    return i->second;
}

void GLTechnique::mapAttrib(GLuint loc, const string& intl_name, const string& name)
{
    glBindAttribLocation(m_Id, loc, intl_name.c_str());
    m_AttrMap[name] = loc;
}

GLTextureVar::GLTextureVar(GLint texunit, const GLSamplerPtr& samp)
    :   m_TexUnit(texunit),
        m_Sampler(samp)
{
}

GLTextureVar::~GLTextureVar()
{
}

void GLTextureVar::set(const TexturePtr& tex)
{
    const GLTexture& _tex = static_cast<const GLTexture&>(*tex);
    _tex.bind(GL_TEXTURE0 + m_TexUnit);
    glUniform1i(m_Location, m_TexUnit);
    m_Sampler->bind(m_TexUnit);
    CHECK_OPENGL();
}

void GLTextureVar::set(TexturePtr* arr, size_t N)
{
    CHECK_OPENGL();
    DynamicArray<GLint> texunits;
    texunits.resize(N);
    for(size_t i = 0; i < N; ++i)
    {
        static_cast<GLTexture&>(*arr[i]).bind(GL_TEXTURE0 + m_TexUnit + i);
        m_Sampler->bind(m_TexUnit + i);
        texunits[i] = m_TexUnit + i;
    }
    glUniform1iv(m_Location, texunits.size(), texunits.get());
    CHECK_OPENGL();
}

GLStateObject::GLStateObject(const MiscStateDescription& desc)
    :   m_DepthClip(desc.DepthClip ? glDisable : glEnable),
        m_Multisample(desc.MultiSampling ? glEnable : glDisable),
        m_Scissor(desc.ScissorTest ? glEnable : glDisable),
        m_AALines(desc.LinesAntiAliasing ? glEnable : glDisable),
        m_DepthBiasFactor(desc.DepthBiasFactor),
        m_DepthBiasUnits(desc.DepthBiasUnits)
{
	switch(desc.PolygonFillMode)
    {
    case CE_FILL_SOLID: m_FillMode = GL_FILL; break;
    case CE_FILL_WIREFRAME: m_FillMode = GL_LINE; break;
    default:
        assert(false); break;
    }

	switch(desc.CullPolygonFace)
    {
    case CE_CULL_NONE: m_CullFace = 0; break;
    case CE_CULL_FRONT: m_CullFace = GL_FRONT; break;
    case CE_CULL_BACK: m_CullFace = GL_BACK; break;
    default:
        assert(false); break;
    }
}

GLSampler::GLSampler()
{
    glGenSamplers(1, &m_Sampler);
}

GLSampler::~GLSampler()
{
    glDeleteSamplers(1, &m_Sampler);
}

void GLSampler::bind(GLuint texunit)
{
    glBindSampler(texunit, m_Sampler);
}

void GLSampler::setParameter(GLenum state, GLenum val)
{
    glSamplerParameteri(m_Sampler, state, val);
}

void GLStateObject::setup()
{
    m_DepthClip(GL_DEPTH_CLAMP);
    m_Multisample(GL_MULTISAMPLE);
    m_Scissor(GL_SCISSOR_TEST);
    m_AALines(GL_LINE_SMOOTH);
    glPolygonOffset(m_DepthBiasFactor, m_DepthBiasUnits);
    if(m_CullFace)
        glEnable(GL_CULL_FACE), glCullFace(m_CullFace);
    else
        glDisable(GL_CULL_FACE);
    glPolygonMode(GL_FRONT_AND_BACK, m_FillMode);
}

GLBlendStateObject::GLBlendStateObject(const BlendStateDescription& desc)
    :   m_Enabled(desc.BlendEnabled),
        m_SrcBlend(getGLBlendFunc(desc.SrcBlend)),
        m_DstBlend(getGLBlendFunc(desc.DstBlend)),
		m_BlendOp(getGLBlendEquation(desc.BlendOp)),
		m_SrcBlendAlpha(getGLBlendFunc(desc.SrcBlendAlpha)),
		m_DstBlendAlpha(getGLBlendFunc(desc.DstBlendAlpha)),
		m_BlendAlphaOp(getGLBlendEquation(desc.BlendAlphaOp))
{

}

void GLBlendStateObject::setup()
{
    if(m_Enabled)
    {
        glEnable(GL_BLEND);
        glBlendFuncSeparate(m_SrcBlend, m_DstBlend, m_SrcBlendAlpha, m_DstBlendAlpha);
        glBlendEquationSeparate(m_BlendOp, m_BlendAlphaOp);
    }
    else
        glDisable(GL_BLEND);
    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); // TODO: set
}

GLenum GLBlendStateObject::getGLBlendFunc(CEBlend blendf)
{
    switch(blendf)
    {
    case CE_BLEND_ZERO: return GL_ZERO;
    case CE_BLEND_ONE: return GL_ONE;
    case CE_BLEND_SRC_COLOR: return GL_SRC_COLOR;
    case CE_BLEND_INV_SRC_COLOR: return GL_ONE_MINUS_SRC_COLOR;
    case CE_BLEND_SRC_ALPHA: return GL_SRC_ALPHA;
    case CE_BLEND_INV_SRC_ALPHA: return GL_ONE_MINUS_SRC_ALPHA;
    case CE_BLEND_DEST_ALPHA: return GL_DST_ALPHA;
    case CE_BLEND_INV_DEST_ALPHA: return GL_ONE_MINUS_DST_ALPHA;
    case CE_BLEND_DEST_COLOR: return GL_DST_COLOR;
    case CE_BLEND_INV_DEST_COLOR: return GL_ONE_MINUS_DST_COLOR;
    case CE_BLEND_SRC_ALPHA_SAT: return GL_SRC_ALPHA_SATURATE;
    case CE_BLEND_SRC1_COLOR: return GL_SRC1_COLOR;
    case CE_BLEND_INV_SRC1_COLOR: return GL_ONE_MINUS_SRC1_COLOR;
    case CE_BLEND_SRC1_ALPHA: return GL_SRC1_ALPHA;
    case CE_BLEND_INV_SRC1_ALPHA: return GL_ONE_MINUS_SRC1_ALPHA;
    }
    assert(false);
    return (GLenum)0;
}

GLenum GLBlendStateObject::getGLBlendEquation(CEBlendOperation blendop)
{
    switch(blendop)
    {
    case CE_BLEND_OP_ADD: return GL_FUNC_ADD;
    case CE_BLEND_OP_SUBTRACT: return GL_FUNC_SUBTRACT;
    case CE_BLEND_OP_REV_SUBTRACT: return GL_FUNC_REVERSE_SUBTRACT;
    case CE_BLEND_OP_MIN: return GL_MIN;
    case CE_BLEND_OP_MAX: return GL_MAX;
    }
    assert(false);
    return (GLenum)0;
}

GLFramebuffer::GLFramebuffer()
{
    glGenFramebuffers(1, &m_FBO);
}

GLFramebuffer::~GLFramebuffer()
{
    glDeleteFramebuffers(1, &m_FBO);
}

void GLFramebuffer::attachDepthStencilTexture(const FBAttachmentInfo& att)
{
    glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);
    m_DepthStencil = att;
    GLenum tex_target = static_cast<GLTexture*>(att.texture.get())->m_Target;
    if(tex_target == GL_TEXTURE_1D)
        glFramebufferTexture1D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
                               GL_TEXTURE_1D, static_cast<GLTexture*>(att.texture.get())->m_TexId, att.level);
    else if(tex_target == GL_TEXTURE_2D)
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
                               GL_TEXTURE_2D, static_cast<GLTexture*>(att.texture.get())->m_TexId, att.level);
    else
        glFramebufferTextureLayer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
                                  static_cast<GLTexture*>(att.texture.get())->m_TexId, att.level, att.layer);
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    CHECK_OPENGL();
}

void GLFramebuffer::attachTextures(const FBAttachmentArray& textures)
{
    glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);
    m_Textures = textures;
    m_BufIds.resize(textures.size());
    for(size_t i = 0; i < textures.size(); ++i)
    {
        GLuint tex_id = static_cast<GLTexture*>(textures[i].texture.get())->m_TexId;
        GLenum tex_target = static_cast<GLTexture*>(textures[i].texture.get())->m_Target;
        GLenum attachment = GL_COLOR_ATTACHMENT0 + i;
        if(tex_target == GL_TEXTURE_1D)
            glFramebufferTexture1D(GL_FRAMEBUFFER, attachment,
                                   GL_TEXTURE_1D, tex_id, textures[i].level);
        else if(tex_target == GL_TEXTURE_2D)
            glFramebufferTexture2D(GL_FRAMEBUFFER, attachment,
                                   GL_TEXTURE_2D, tex_id, textures[i].level);
        else
            glFramebufferTextureLayer(GL_FRAMEBUFFER, attachment,
                                      tex_id, textures[i].level, textures[i].layer);
        m_BufIds[i] = attachment;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    CHECK_OPENGL();
}

void GLFramebuffer::bind()
{
    glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);
    glDrawBuffers(m_BufIds.size(), m_BufIds.get());
    glReadBuffer(m_BufIds[0]);
}

void GLFramebuffer::bindDraw()
{
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_FBO);
    glDrawBuffers(m_BufIds.size(), m_BufIds.get());
}

void GLFramebuffer::bindRead()
{
    glBindFramebuffer(GL_READ_FRAMEBUFFER, m_FBO);
    glReadBuffer(m_BufIds[0]);
}


bool GLFramebuffer::validate()
{
    bool status = false;
    glBindFramebuffer(GL_FRAMEBUFFER, m_FBO);
    GLenum err = glCheckFramebufferStatus(GL_FRAMEBUFFER);
    CHECK_OPENGL();
    switch(err)
    {
    case GL_FRAMEBUFFER_UNDEFINED: Log::message(CE_LOG_ERROR, "OpenGL: Default framebufffer does not exist"); status = false; break;
    case GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT: Log::message(CE_LOG_ERROR, "OpenGL: Incomplete attachment points"); status = false; break;
    case GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT: Log::message(CE_LOG_ERROR, "OpenGL: Framebuffer does not have any attached image"); status = false; break;
    case GL_FRAMEBUFFER_INCOMPLETE_DRAW_BUFFER: Log::message(CE_LOG_ERROR, "OpenGL: Incomplete draw buffer"); status = false; break;
    case GL_FRAMEBUFFER_INCOMPLETE_READ_BUFFER: Log::message(CE_LOG_ERROR, "OpenGL: Incomplete read buffer"); status = false; break;
    case GL_FRAMEBUFFER_UNSUPPORTED: Log::message(CE_LOG_ERROR, "OpenGL: Unsupported attachments"); status = false; break;
    case GL_FRAMEBUFFER_INCOMPLETE_MULTISAMPLE: Log::message(CE_LOG_ERROR, "OpenGL: Incomplete multisample"); status = false; break;
    case GL_FRAMEBUFFER_INCOMPLETE_LAYER_TARGETS: Log::message(CE_LOG_ERROR, "OpenGL: Incomplete layer targets"); status = false; break;
    case GL_FRAMEBUFFER_COMPLETE: status = true; break;
    default: Log::message(CE_LOG_ERROR, "OpenGL: Unknown framebuffer error"); status = false; break;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    return status;
}

FBAttachmentInfo& GLFramebuffer::getColorAttachment(size_t idx)
{
    return m_Textures[idx];
}

FBAttachmentInfo& GLFramebuffer::getDepthStencil()
{
    return m_DepthStencil;
}

GLDepthStencilStateObject::GLDepthStencilStateObject(const DepthStencilStateDescription& desc)
    :   m_DepthTest(desc.DepthTest ? glEnable : glDisable),
        m_StencilTest(desc.StencilTest ? glEnable : glDisable),
        m_DepthMask(desc.DepthMask ? GL_TRUE : GL_FALSE),
        m_DepthFunc(getCompFunc(desc.DepthFunc)),
		m_SFrontSFail(getStencilOp(desc.FrontStencilFail)),
		m_SFrontDPFail(getStencilOp(desc.FrontStencilDepthFail)),
		m_SFrontDPPass(getStencilOp(desc.FrontStencilDepthPass)),
		m_SFrontFunc(getCompFunc(desc.FrontStencilFunc)),
		m_SBackSFail(getStencilOp(desc.BackStencilFail)),
		m_SBackDPFail(getStencilOp(desc.BackStencilDepthFail)),
		m_SBackDPPass(getStencilOp(desc.BackStencilDepthPass)),
		m_SBackFunc(getCompFunc(desc.BackStencilFunc)),
		m_StencilMask(desc.StencilMask),
		m_StencilRef(desc.StencilRef)
{
}

void GLDepthStencilStateObject::setup()
{
    m_DepthTest(GL_DEPTH_TEST);
    m_StencilTest(GL_STENCIL_TEST);
    glDepthMask(m_DepthMask);
    glDepthFunc(m_DepthFunc);
    glStencilFuncSeparate(GL_FRONT, m_SFrontFunc, m_StencilRef, ~0);
    glStencilMask(m_StencilMask);
    glStencilOpSeparate(GL_FRONT, m_SFrontSFail, m_SFrontDPFail, m_SFrontDPPass);
    glStencilOpSeparate(GL_BACK, m_SBackSFail, m_SBackDPFail, m_SBackDPPass);
}

GLenum GLDepthStencilStateObject::getCompFunc(CECompFunc sfunc)
{
    switch(sfunc)
    {
    case CE_CMP_NEVER: return GL_NEVER;
    case CE_CMP_LESS: return GL_LESS;
    case CE_CMP_EQUAL: return GL_EQUAL;
    case CE_CMP_LESS_EQUAL: return GL_LEQUAL;
    case CE_CMP_GREATER: return GL_GREATER;
    case CE_CMP_NOT_EQUAL: return GL_NOTEQUAL;
    case CE_CMP_GREATER_EQUAL: return GL_GEQUAL;
    case CE_CMP_ALWAYS: return GL_ALWAYS;
    }
    assert(false);
    return 0;
}

GLenum GLDepthStencilStateObject::getStencilOp(CEStencilOperation sop)
{
    switch(sop)
    {
    case CE_STENCIL_OP_KEEP: return GL_KEEP;
    case CE_STENCIL_OP_ZERO: return GL_ZERO;
    case CE_STENCIL_OP_REPLACE: return GL_REPLACE;
    case CE_STENCIL_OP_INCR_SAT: return GL_INCR_WRAP;
    case CE_STENCIL_OP_DECR_SAT: return GL_DECR_WRAP;
    case CE_STENCIL_OP_INVERT: return GL_INVERT;
    case CE_STENCIL_OP_INCR: return GL_INCR;
    case CE_STENCIL_OP_DECR: return GL_DECR;
    }
    assert(false);
    return 0;
}
}
